﻿// See https://aka.ms/new-console-template for more information

using System.Reflection;
using System.Runtime.CompilerServices;
using Dapper;
using Scriban;

var type = typeof(Dapper.SqlMapper);

var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Static);

var prefix = new string[] { "Query", "Execute" };

const string syncTargetFile = @"..\..\..\..\DapperExtensions\SqlMapperExtensions.Sync.cs";

const string asyncTargetFile = @"..\..\..\..\DapperExtensions\SqlMapperExtensions.Async.cs";

var targetMethods = methods.Where(a => prefix.Any(p => a.Name.StartsWith(p)))
    .Select(a => new MethodContext(a))
    .ToList();


var template = Template.Parse(File.ReadAllText("template.sciban"));

File.WriteAllText(asyncTargetFile, template.Render(new { methods = targetMethods.Where(a => a.IsAsync).ToList() }));

File.WriteAllText(syncTargetFile, template.Render(new { methods = targetMethods.Where(a => !a.IsAsync).ToList() }));

class ParameterContext
{
    public string Type { get; private set; }

    public string Name { get; private set; }

    public bool HasDefaultValue { get; }

    public string? DefaultValue { get; }

    public ParameterContext(string name, string type, bool hasDefaultValue, object? defaultValue)
    {
        Type = type;
        Name = name;
        HasDefaultValue = hasDefaultValue;

        if (HasDefaultValue)
        {
            if (defaultValue == null)
            {
                DefaultValue = "null";
            }
            else if (defaultValue is true)
            {
                DefaultValue = "true";
            }
            else if (defaultValue is false)
            {
                DefaultValue = "false";
            }
            else if (defaultValue is string s)
            {
                DefaultValue = '"' + s + '"';
            }
            else
            {
                throw new NotSupportedException("DefaultValue");
            }
        }
    }
}

class MethodContext
{
    public bool IsAsync { get; }

    public bool IsGenericMethod { get; }

    public string FullName { get; }

    public string Name { get; }

    public string Return { get; }

    public IReadOnlyList<ParameterContext> Parameters { get; }

    public string ParameterString => string.Join(", ", Parameters.Select(p =>
        p.HasDefaultValue ? $"{p.Type} {p.Name} = {p.DefaultValue}" : $"{p.Type} {p.Name}"));

    public string CallParameterString => string.Join(", ", Parameters.Select(p =>
    {
        if (p.Name == "sql") return "ctx.CommandText";
        if (p.Name == "param") return "ctx.Parameters";
        return p.Name;
    }));

    public int CommandIndex { get; private set; }

    public string? CommandName { get; private set; }

    public MethodContext(MethodInfo method)
    {
        #region Name

        Name = method.Name;
        if (method.IsGenericMethod)
        {
            IsGenericMethod = true;

            FullName = $"{Name}<{string.Join(", ", method.GetGenericArguments().Select(x => x.Name))}>";
        }
        else
        {
            FullName = Name;
        }

        #endregion

        #region Return

        var isDynamic = method.ReturnTypeCustomAttributes.GetCustomAttributes(typeof(DynamicAttribute), true).Length > 0;

        if (method.ReturnType.Namespace == "System.Threading.Tasks")
        {
            IsAsync = true;

            Return = MakeReturnName(method.ReturnType.GenericTypeArguments[0], isDynamic);
            Return = $"Task<{Return}>";
        }
        else
        {
            Return = MakeReturnName(method.ReturnType, isDynamic);
        }

        #endregion

        #region Parameters

        CommandIndex = -1;

        Parameters = method.GetParameters()
            .Select((p, index) =>
            {
                if (p.ParameterType == typeof(CommandDefinition))
                {
                    CommandIndex = index;
                    CommandName = p.Name!;
                }

                return new ParameterContext(p.Name!, MakeParameterName(p.ParameterType), p.HasDefaultValue,
                    p.RawDefaultValue);
            })
            .ToList();

        #endregion
    }

    static bool TryMakeSimpleTypeName(Type type, out string? name)
    {
        if (type == typeof(int))
        {
            name = "int";
            return true;
        }

        if (type == typeof(long))
        {
            name = "long";
            return true;
        }

        if (type == typeof(bool))
        {
            name = "bool";
            return true;
        }

        if (type == typeof(string))
        {
            name = "string";
            return true;
        }

        if (type == typeof(object))
        {
            name = "object";
            return true;
        }

        if (type == typeof(Task))
        {
            name = "Task";
            return true;
        }

        if (type == typeof(string[]))
        {
            name = "string[]";
            return true;
        }

        if (type == typeof(object[]))
        {
            name = "object[]";
            return true;
        }

        if (type.Name.StartsWith("Nullable`"))
        {
            var genericName = MakeParameterName(type.GenericTypeArguments[0]);
            name = genericName + '?';
            return true;
        }

        name = null;
        return false;
    }

    static bool TryMakeTypeName(Type type, out string? name)
    {
        if (TryMakeSimpleTypeName(type, out name)) return true;

        if (type.IsGenericType)
        {
            name = type.Name[..^2];
            var arg = type.GetGenericArguments()
                .Select(x =>
                {
                    if (TryMakeSimpleTypeName(x, out var xName)) return xName;
                    return x.Name;
                })
                .ToList();
            name = $"{name}<{string.Join(", ", arg)}>";
            return true;
        }

        name = null;
        return false;
    }

    static string MakeReturnName(Type parameter, bool isDynamic)
    {
        if (isDynamic)
        {
            if (parameter.IsGenericType)
            {
                if (parameter.GenericTypeArguments.Length == 1)
                {
                    var generic = parameter.Name[..^2];
                    return $"{generic}<dynamic>";
                }

                throw new NotSupportedException("Dynamic");
            }
            return "dynamic";
        }

        if (TryMakeTypeName(parameter, out var name))
        {
            return name!;
        }

        if (parameter.FullName?.StartsWith("Dapper.SqlMapper+") == true)
        {
            name = parameter.FullName.Replace('+', '.');
            return name;
        }

        name = parameter.Name;
        if (string.IsNullOrEmpty(name))
        {
            throw new NotSupportedException("ReturnName");
        }

        return name;
    }

    static string MakeParameterName(Type type)
    {
        string name;
        if (type.Name.StartsWith("Func`"))
        {
            name = string.Join(", ", type.GenericTypeArguments.Select(x =>
            {
                if (x.Name == "Object") return "object";
                if (x.Name == "Object[]") return "object[]";
                return x.Name;
            }));

            return $"Func<{name}>";
        }

        if (TryMakeTypeName(type, out name!))
        {
            return name!;
        }

        return type.Name;
    }
}